import uuid
import time
from freezegun import freeze_time
from datetime import datetime
from unittest.mock import patch, ANY

from rest_api.rest_api_server.exceptions import Err
from rest_api.rest_api_server.tests.unittests.test_api_base import TestApiBase
from tools.optscale_exceptions.common_exc import HeraldException
from tools.optscale_exceptions.http_exc import OptHTTPError


class TestInviteApi(TestApiBase):

    def setUp(self, version='v2'):
        super().setUp(version)
        self.user_id = self.gen_id()
        self._mock_auth_user(self.user_id)
        patch(
            'rest_api.rest_api_server.controllers.invite.'
            'InviteController.check_user_exists',
            return_value=(False, {})).start()
        patch(
            'rest_api.rest_api_server.controllers.invite.'
            'InviteController.get_invite_expiration_days',
            return_value=30).start()
        _, root_organization = self.client.organization_create(
            {'name': 'root org'})
        self.org_name = root_organization['name']
        self.org_id = root_organization['id']
        self.root_pool_id = root_organization['pool_id']
        _, root_organization2 = self.client.organization_create(
            {'name': 'root org 2'})
        _, pool1 = self.client.pool_create(root_organization['id'], {
            'name': 'pool1', 'parent_id': root_organization['pool_id']})
        self.pool_id_1 = pool1['id']
        _, pool2 = self.client.pool_create(root_organization2['id'], {
            'name': 'pool2', 'parent_id': root_organization2['pool_id']})
        _, pool3 = self.client.pool_create(root_organization2['id'], {
            'name': 'pool3', 'parent_id': pool2['id']})
        self.pool_id_2 = pool3['id']
        self.owner_email = 'my@email.com'
        self.email_1 = 'test@email.com'
        self.email_2 = 'anotheruser@email.com'
        email_request = [
            {'scope_id': self.pool_id_1, 'scope_type': 'pool'},
            {'scope_id': self.pool_id_2, 'purpose': 'optscale_engineer',
             'scope_type': 'pool'}
        ]
        self.one_email_correct_body = {
            'invites': {
                self.email_1: email_request
            }
        }
        self.correct_body = {
            'invites': {
                self.email_1: email_request,
                self.email_2: [
                    {'scope_id': self.org_id, 'scope_type': 'organization',
                     'purpose': 'optscale_engineer'}
                ]
            }
        }

        self.user_assignments = [
            {
                'assignment_id': self.gen_id(),
                'assignment_resource': root_organization['id'],
                'assignment_resource_type': 2,
                'role_id': 3,
                'role_name': 'Manager',
                'role_scope': None
            },
            {
                'assignment_id': self.gen_id(),
                'assignment_resource': root_organization2['id'],
                'assignment_resource_type': 2,
                'role_id': 3,
                'role_name': 'Manager',
                'role_scope': None
            }]

        patch('rest_api.rest_api_server.controllers.invite.'
              'InviteController.get_user_auth_assignments',
              return_value=self.user_assignments).start()
        patch('rest_api.rest_api_server.controllers.employee.'
              'EmployeeController.notification_domain_blacklist').start()
        self.mock_user_info(self.owner_email)

    def mock_user_info(self, email, name='default'):
        value = {
                'id': self.user_id,
                'display_name': name,
                'email': email,
                'is_password_autogenerated': False
            }
        patch('rest_api.rest_api_server.handlers.v1.base.'
              'BaseAuthHandler._get_user_info',
              return_value=value).start()
        patch('rest_api.rest_api_server.controllers.base.'
              'BaseController.get_user_info',
              return_value=value).start()

    def test_create_duplicate_scope_id(self):
        self.correct_body['invites'][self.email_1].append(
            {'scope_id': self.pool_id_2, 'scope_type': 'pool'})
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 400)
        self.assertEqual(response['error']['reason'],
                         'Duplicated scope_id for email %s' % self.email_1)

    def test_create_without_assignment(self):
        body = {
            'invites': {
                self.email_2: []
            }
        }
        code, response = self.client.invite_create(body)
        self.assertEqual(code, 400)
        self.assertEqual(
            response['error']['reason'],
            'User with email %s is not assigned to any organization' %
            self.email_2)

    def test_assignment_without_scope(self):
        body = {
            'invites': {
                self.email_2: [{
                    'purpose': 'optscale_engineer',
                    'scope_type': 'organization'
                }]
            }
        }
        code, response = self.client.invite_create(body)
        self.assertEqual(code, 400)
        self.verify_error_code(response, 'OE0216')
        self.assertEqual(response['error']['reason'],
                         'scope_id is not provided')

    def test_assignment_invalid_purpose(self):
        body = {
            'invites': {
                self.email_2: [{
                    'scope_id': self.pool_id_2,
                    'purpose': 'invalid',
                    'scope_type': 'pool',
                }]
            }
        }
        code, response = self.client.invite_create(body)
        self.assertEqual(code, 400)
        self.assertEqual(response['error']['reason'],
                         "Bad request: 'invalid' is not a valid RolePurposes")

    def test_create(self):
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 201)

    def test_employee_invited_event(self):
        p_publish_activities = patch(
            'rest_api.rest_api_server.controllers.base.BaseController.'
            'publish_activities_task'
        ).start()
        fmt_args = {
            'initiator_name': 'user',
            'initiator_email': 'user@ema.il',
            'user_email': self.email_2,
            'scope_purposes': '%s: %s' % (
                # org name is equal to root pool name
                self.org_name, 'optscale_engineer')
        }
        self.p_get_user_info.return_value = {
            'display_name': fmt_args['initiator_name'], 'id': self._user_id,
            'email': fmt_args['initiator_email']}
        code, _ = self.client.invite_create({'invites': {
            self.email_2: [
                {'scope_id': self.root_pool_id, 'scope_type': 'pool',
                 'purpose': 'optscale_engineer'}
            ]}})
        activity_param_tuples = self.get_publish_activity_tuple(
            self.org_id, self.org_id, 'organization',
            'employee_invited', {
                'object_name': self.org_name,
                'email': self.email_2,
                'scope_purposes': '%s: %s' % (self.org_name,
                                              'engineer')
            })
        p_publish_activities.assert_called_once_with(
            *activity_param_tuples, add_token=True
        )

    def test_employee_member_invited_event(self):
        p_publish_activities = patch(
            'rest_api.rest_api_server.controllers.base.BaseController.'
            'publish_activities_task'
        ).start()
        fmt_args = {
            'initiator_name': 'user',
            'initiator_email': 'user@ema.il',
            'user_email': self.email_2,
            'scope_purposes': '%s: %s' % (
                # org name is equal to root pool name
                self.org_name, 'optscale_member')
        }
        self.p_get_user_info.return_value = {
            'display_name': fmt_args['initiator_name'], 'id': self._user_id,
            'email': fmt_args['initiator_email']}
        code, _ = self.client.invite_create({'invites': {
            self.email_2: [
                {'scope_id': self.org_id, 'scope_type': 'organization',
                 'purpose': None}
            ]}})
        activity_param_tuples = self.get_publish_activity_tuple(
            self.org_id, self.org_id, 'organization',
            'employee_invited', {
                'object_name': self.org_name,
                'email': self.email_2,
                'scope_purposes': '%s: %s' % (self.org_name, 'member')
            })
        p_publish_activities.assert_called_once_with(
            *activity_param_tuples, add_token=True
        )

    def test_create_empty(self):
        code, response = self.client.invite_create({})
        self.assertEqual(code, 400)
        self.assertEqual(response['error']['reason'],
                         'invites is not provided')

        code, response = self.client.invite_create({'invites': []})
        self.assertEqual(code, 400)
        self.assertEqual(response['error']['reason'],
                         'invites is not provided')

    def test_create_invalid_invites(self):
        code, response = self.client.invite_create({'invites': 'string'})
        self.assertEqual(code, 400)
        self.assertEqual(response['error']['reason'],
                         'invites should be a dictionary')

    def test_list_by_token(self):
        patch('rest_api.rest_api_server.handlers.v1.base.BaseAuthHandler.'
              'check_cluster_secret', return_value=False).start()
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 201)
        self.mock_user_info(self.email_1)
        code, response = self.client.invite_list()
        self.assertEqual(code, 200)
        invites = response['invites']
        self.assertEqual(len(invites), 1)
        self.assertEqual(invites[0]['email'], self.email_1)
        self.assertEqual(len(invites[0]['invite_assignments']), 4)
        self.mock_user_info(self.email_2)
        code, response = self.client.invite_list()
        self.assertEqual(code, 200)
        invites = response['invites']
        self.assertEqual(len(invites), 1)
        self.assertEqual(invites[0]['email'], self.email_2)
        self.assertEqual(invites[0]['owner_name'], 'default')
        self.assertEqual(len(invites[0]['invite_assignments']), 1)

    def test_list_by_secret(self):
        patch('rest_api.rest_api_server.handlers.v1.base.BaseAuthHandler.'
              'check_cluster_secret', return_value=True).start()
        code, response = self.client.invite_create(self.correct_body)
        self.mock_user_info(self.email_1)
        self.assertEqual(code, 201)
        code, response = self.client.invite_list()
        self.assertEqual(code, 200)
        self.assertEqual(len(response['invites']), 2)

    def test_list_by_email(self):
        patch('rest_api.rest_api_server.handlers.v1.base.BaseAuthHandler.'
              'check_cluster_secret', return_value=True).start()
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 201)
        self.mock_user_info(self.email_1)
        code, response = self.client.invite_list(email=self.email_1)
        self.assertEqual(code, 200)
        invites = response['invites']
        self.assertEqual(len(invites), 1)
        self.assertEqual(invites[0]['email'], self.email_1)

    def test_list_organization_id(self):
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 201)
        _, organization = self.client.organization_create(
            {'name': 'org2'})
        self.assertEqual(code, 201)
        user_assignments = self.user_assignments.copy()
        user_assignments.append({
            'assignment_id': self.gen_id(),
            'assignment_resource': organization['id'],
            'assignment_resource_type': 2,
            'role_id': 3,
            'role_name': 'Manager',
            'role_scope': None
        })
        patch('rest_api.rest_api_server.controllers.invite.'
              'InviteController.get_user_auth_assignments',
              return_value=user_assignments).start()
        params = {
            'invites': {
                'org2@email.com': [
                    {'scope_id': organization['pool_id'],
                     'scope_type': 'pool',
                     'purpose': 'optscale_engineer'}
                ]
            }
        }
        code, response = self.client.invite_create(params)
        self.assertEqual(code, 201)
        patch('rest_api.rest_api_server.handlers.v1.base.BaseAuthHandler.'
              'check_cluster_secret', return_value=True).start()
        code, response = self.client.invite_list(
            organization_id=organization['id'])
        self.assertEqual(code, 200)
        self.assertEqual(len(response['invites']), 1)
        self.assertEqual(response['invites'][0]['email'],
                         list(params['invites'])[0])

    def test_accept_invite(self):
        body = {
            'invites': {
                self.email_1: [
                    {'scope_id': self.pool_id_1, 'scope_type': 'pool'},
                    {'scope_id': self.pool_id_2,
                     'purpose': 'optscale_engineer', 'scope_type': 'pool'}
                ]
            }
        }
        code, invites = self.client.invite_create(body)
        invite_id = invites['invites'][0]['id']
        self.assertEqual(code, 201)
        self.mock_user_info(self.email_2)
        code, response = self.client.invite_accept(invite_id)
        self.assertEqual(code, 404)
        self.mock_user_info(self.email_1)
        code, response = self.client.invite_accept(invite_id)
        self.assertEqual(code, 204)

        code, resp = self.client.employee_list(self.org_id)
        self.assertEqual(code, 200)
        auth_user_ids = [self.user_id, self._user_id]
        emp_auth_ids = [resp['employees'][0]['auth_user_id'],
                        resp['employees'][1]['auth_user_id']]
        # default employee + invited employee
        self.assertEqual(len(resp['employees']), 2)
        self.assertCountEqual(emp_auth_ids, auth_user_ids)

    def test_decline_invite(self):
        body = {
            'invites': {
                self.email_1: [
                    {'scope_id': self.org_id, 'scope_type': 'organization'},
                    {'scope_id': self.pool_id_2,
                     'purpose': 'optscale_engineer', 'scope_type': 'pool'}
                ]
            }
        }
        code, invites = self.client.invite_create(body)
        invite_id = invites['invites'][0]['id']
        self.assertEqual(code, 201)
        self.mock_user_info(self.email_2)
        code, response = self.client.invite_decline(invite_id)
        self.assertEqual(code, 404)
        self.mock_user_info(self.email_1)
        code, response = self.client.invite_decline(invite_id)
        self.assertEqual(code, 204)

    def test_patch_without_action(self):
        code, response = self.client.patch('invites/%s' % uuid.uuid4(), {})
        self.assertEqual(code, 400)
        self.assertEqual(response['error']['reason'], 'action is not provided')

    def test_patch_invalid_action(self):
        code, response = self.client.patch('invites/%s' % uuid.uuid4(),
                                           {'action': 'invalid'})
        self.assertEqual(code, 400)
        self.assertEqual(response['error']['reason'],
                         'Action "invalid" is not supported')

        code, response = self.client.patch('invites/%s' % uuid.uuid4(),
                                           {'action': 123})
        self.assertEqual(code, 400)
        self.assertEqual(response['error']['reason'],
                         'action should be a string')

    def test_accept_wrong_invite(self):
        self.mock_user_info(self.email_2)
        fake_invite_id = uuid.uuid4()
        code, response = self.client.patch('invites/%s' % fake_invite_id,
                                           {'action': 'accept'})
        self.assertEqual(code, 404)
        self.assertEqual(response['error']['reason'],
                         'Invite %s not found' % fake_invite_id)

    def test_get_invite(self):
        body = {
            'invites': {
                self.email_1: [
                    {'scope_id': self.org_id, 'scope_type': 'organization',
                     'purpose': 'optscale_manager'}
                ]
            }
        }
        self.mock_user_info(self.email_2)
        code, invites = self.client.invite_create(body)
        invite_id = invites['invites'][0]['id']
        created_assignment_id = invites['invites'][0]['invite_assignments'][0]['id']
        self.assertEqual(code, 201)
        code, response = self.client.invite_get(invite_id)
        self.assertEqual(code, 404)

        self.mock_user_info(self.email_1)
        code, response = self.client.invite_get(invite_id)
        self.assertEqual(code, 200)
        self.assertEqual(response['email'], self.email_1)
        self.assertEqual(response['owner_name'], 'default')
        self.assertEqual(len(response['invite_assignments']), 1)
        assignment = response['invite_assignments'][0]
        self.assertEqual(assignment['scope_id'], self.org_id)
        self.assertEqual(assignment['scope_name'], 'root org')
        self.assertEqual(assignment['scope_type'], 'organization')
        self.assertEqual(assignment['id'], created_assignment_id)
        self.assertEqual(response['organization'], 'root org')
        self.assertEqual(response['organization_id'], self.org_id)

    def test_expired_token(self):
        self.p_get_meta_by_token.return_value = {
            'user_id': self.user_id, 'valid_until': time.time() - 1}
        code, response = self.client.invite_get(str(uuid.uuid4()))
        self.assertEqual(code, 401)

    def test_invalid_token(self):
        token = str(uuid.uuid4())
        self.p_get_meta_by_token.return_value = {token: {}}
        code, response = self.client.invite_get(token)
        self.assertEqual(code, 401)

    def test_accept_second_invite(self):
        body = {
            'invites': {
                self.email_1: [
                    {'scope_id': self.pool_id_1,
                     'purpose': 'optscale_engineer', 'scope_type': 'pool'}
                ]
            }
        }
        code, invites = self.client.invite_create(body)
        invite_1_id = invites['invites'][0]['id']
        body['invites'][self.email_1][0]['purpose'] = 'optscale_manager'
        code, invites = self.client.invite_create(body)
        invite_2_id = invites['invites'][0]['id']
        self.mock_user_info(self.email_1)
        code, response = self.client.invite_accept(invite_1_id)
        self.assertEqual(code, 204)
        code, response = self.client.invite_accept(invite_2_id)
        self.assertEqual(code, 204)

    def test_self_invite(self):
        self.mock_user_info(self.email_1)
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 403)
        self.assertEqual(response['error']['reason'],
                         'Invite yourself is forbidden')

    def test_send_email_exception(self):
        self.mock_email_send_enable()
        patch('optscale_client.herald_client.client_v2.Client.email_send',
              side_effect=HeraldException(Err.OEXXXX, [])).start()
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 400)

    def test_send_email(self):
        send_email_patch = self.mock_email_send_enable()
        body = {
            'invites': {
                self.email_1: [
                    {'scope_id': self.pool_id_1, 'scope_type': 'pool'}
                ]
            }
        }
        code, response = self.client.invite_create(body)
        self.assertEqual(code, 201)
        send_email_patch.assert_called_once_with(
            ['test@email.com'],
            'OptScale invitation notification',
            template_params={
                'texts': {
                    'organization': {
                        'id': self.org_id,
                        'name': 'root org',
                        'currency_code': '$'
                    },
                },
                'links': {
                    'login_button': ANY
                }
            },
            template_type='invite')

    def test_create_invite_show_link(self):
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 201)
        self.assertIsNone(response['invites'][0].get('url'))

        self.correct_body.update({'show_link': True})
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 201)
        self.assertIsNotNone(response['invites'][0].get('url'))

        self.client.secret = None
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 403)
        self.assertEqual(response['error']['reason'], 'Bad secret')

    def test_expired_invite(self):
        body = {
            'invites': {
                'first@email.com': [
                    {'scope_id': self.pool_id_1, 'scope_type': 'pool'}
                ]
            }
        }
        with freeze_time(datetime(2020, 1, 1)):
            code, response = self.client.invite_create(body)
            invites = response['invites']
            self.assertEqual(len(invites), 1)
            invite_1_id = invites[0]['id']
            self.mock_user_info('first@email.com')
            code, invite_1 = self.client.invite_get(invite_1_id)
            self.assertEqual(code, 200)
            self.assertEqual(invite_1['id'], invite_1_id)
            code, invites = self.client.invite_list()
            self.assertEqual(code, 200)
            self.assertEqual(len(invites['invites']), 1)
            self.assertEqual(invites['invites'][0]['id'], invite_1_id)
        self.mock_user_info(self.owner_email)
        with freeze_time(datetime(2020, 1, 15)):
            code, response = self.client.invite_create(body)
            invites = response['invites']
            self.assertEqual(len(invites), 1)
            invite_2_id = invites[0]['id']
            self.mock_user_info('first@email.com')
            code, invite_1 = self.client.invite_get(invite_1_id)
            self.assertEqual(code, 200)
            code, invite_2 = self.client.invite_get(invite_2_id)
            self.assertEqual(code, 200)
            self.assertEqual(invite_2['id'], invite_2_id)
            code, invites = self.client.invite_list()
            self.assertEqual(code, 200)
            self.assertEqual(len(invites['invites']), 2)
            for invite in invites['invites']:
                self.assertTrue(invite['id'] in [invite_1_id, invite_2_id])
        with freeze_time(datetime(2020, 2, 5)):
            code, invite_1 = self.client.invite_get(invite_1_id)
            self.assertEqual(code, 404)
            code, invite_2 = self.client.invite_get(invite_2_id)
            self.assertEqual(code, 200)
            code, invites = self.client.invite_list()
            self.assertEqual(code, 200)
            self.assertEqual(len(invites['invites']), 1)
            self.assertEqual(invites['invites'][0]['id'], invite_2_id)
            code, resp = self.client.invite_accept(invite_1_id)
            self.assertEqual(code, 404)
        with freeze_time(datetime(2020, 2, 16)):
            code, invite_2 = self.client.invite_get(invite_2_id)
            self.assertEqual(code, 404)
            code, invites = self.client.invite_list()
            self.assertEqual(code, 200)
            self.assertEqual(len(invites['invites']), 0)
            code, resp = self.client.invite_decline(invite_2_id)
            self.assertEqual(code, 404)

    def test_employee_create_email(self):
        body = {
            'invites': {
                self.email_1: [
                    {'scope_id': self.pool_id_1,
                     'purpose': 'optscale_engineer', 'scope_type': 'pool'}
                ]
            }
        }
        code, invites = self.client.invite_create(body)
        invite_id = invites['invites'][0]['id']
        self.assertEqual(code, 201)
        self.mock_user_info(self.email_1)
        code, response = self.client.invite_accept(invite_id)
        self.assertEqual(code, 204)

    def test_create_invite_not_existing_org(self):
        body = {
            'invites': {
                'some@email.com': [
                    {'scope_id': str(uuid.uuid4()),
                     'scope_type': 'organization'}
                ]
            }
        }
        code, response = self.client.invite_create(body)
        self.assertEqual(code, 400)
        self.assertEqual(response['error']['error_code'], 'OE0005')

        patch('rest_api.rest_api_server.handlers.v1.base.BaseAuthHandler.'
              'check_permissions',
              side_effect=OptHTTPError(404, Err.OE0002, ['Partner', '123'])
              ).start()
        code, response = self.client.invite_create(body)
        self.assertEqual(code, 400)
        self.assertEqual(response['error']['error_code'], 'OE0005')

    def test_invite_deleted_pool(self):
        body = {
            'invites': {
                self.email_1: [
                    {'scope_id': self.pool_id_1, 'scope_type': 'pool'}
                ]
            }
        }
        code, invites = self.client.invite_create(body)
        self.assertEqual(code, 201)
        self.assertEqual(len(invites['invites'][0]['invite_assignments']), 2)
        for i in invites['invites'][0]['invite_assignments']:
            self.assertTrue(i['scope_id'] in [self.pool_id_1, self.org_id])

        code, _ = self.client.pool_delete(self.pool_id_1)
        self.assertEqual(code, 204)

        self.mock_user_info(self.email_1)
        code, invite = self.client.invite_get(invites['invites'][0]['id'])
        self.assertEqual(code, 200)
        self.assertEqual(len(invite['invite_assignments']), 1)
        self.assertTrue(
            invite['invite_assignments'][0]['scope_id'] == self.org_id)

    def test_invite_several_scopes(self):
        code, pool_2 = self.client.pool_create(self.org_id, {
            'name': 'pool2', 'parent_id': self.root_pool_id})
        self.assertEqual(code, 201)
        code, pool_3 = self.client.pool_create(self.org_id, {
            'name': 'pool3', 'parent_id': self.root_pool_id})
        self.assertEqual(code, 201)
        body = {
            'invites': {
                self.email_1: [
                    {'scope_id': self.pool_id_1, 'scope_type': 'pool'},
                    {'scope_id': pool_2['id'], 'scope_type': 'pool'},
                    {'scope_id': pool_3['id'], 'scope_type': 'pool'}
                ]
            }
        }
        code, resp = self.client.invite_create(body)
        self.assertEqual(code, 201)
        invites = resp['invites']
        self.assertEqual(len(invites), 1)
        invite_assignments = invites[0]['invite_assignments']
        self.assertEqual(len(invite_assignments), 4)

    def test_not_org_manager_user(self):
        patch('rest_api.rest_api_server.controllers.invite.InviteController.get_user_auth_assignments',
              return_value=[]).start()
        code, response = self.client.invite_create(self.correct_body)
        self.assertEqual(code, 403)
